﻿#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

using Patterns.Mapping;
using Patterns.SafeCode;

namespace Patterns.Collections
{
	/// <summary>
	/// 	Provides ease-of-use extensions to dictionary types.
	/// </summary>
	public static class DictionaryExtensions
	{
		/// <summary>
		/// 	Converts the object into a property bag, or dictionary of property names and values.
		/// </summary>
		/// <param name = "instance">The instance.</param>
		/// <returns>
		/// 	A dictionary of property names and their corresponding values.
		/// </returns>
		/// <remarks>
		/// 	Static, Instance, Public, and Private properties are included, but only properties
		/// 	whose values can be read are included in the result. See
		/// 	<see cref = "P:System.Reflection.PropertyInfo.CanRead" /> for details.
		/// 	<br />
		/// 	The resulting dictionary uses case-insensitive key comparison. This is to
		/// 	greater facilitate matching scenarios where columns, fields, or other forms of
		/// 	data persistence have been manipulated to have a different casing from the
		/// 	original value.
		/// </remarks>
		public static IDictionary<string, object> AsPropertyBag(this object instance)
		{
			if (instance == null) return null;

			return instance.GetType().FindProperties(propertyFilter: property => property.CanRead)
				.ToDictionary(property => property.Name,
				              property => ExtractPropertyBagValue(property.GetValue(instance, null), property.PropertyType),
				              StringComparer.OrdinalIgnoreCase);
		}

		/// <summary>
		/// Pulls the specified value from the dictionary.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="key">The key.</param>
		/// <returns>The specified value.</returns>
		/// <remarks>
		/// This method is an abstraction of the <see cref="GetValue"/> method;
		/// it is presented for use in simple scenarios where <see cref="Expression"/> values
		/// are being evaluated.
		/// </remarks>
		public static object Pull(this IDictionary items, object key)
		{
			return items.GetValue(key);
		}

		/// <summary>
		/// 	Gets the value at the specified key.
		/// </summary>
		/// <param name = "items">The items.</param>
		/// <param name = "key">The key.</param>
		/// <param name = "initializer">The initializer (optional).</param>
		/// <param name = "errorHandler">The error handler (optional).</param>
		/// <returns>
		/// 	The requested value.
		/// </returns>
		/// <remarks>
		/// 	Error handling is provided by <see cref = "Try.HandleErrors.DefaultStrategy" />
		/// 	if no error handler is specified.
		/// </remarks>
		/// <seealso cref = "Try" />
		public static object GetValue(this IDictionary items, object key, Func<object> initializer = null, Action<Exception> errorHandler = null)
		{
			errorHandler = errorHandler ?? Try.HandleErrors.DefaultStrategy;

			bool exists = Try.Get(() => items.Contains(key));

			if (!exists && initializer != null)
			{
				exists = true;
				Try.Do(() => items[key] = initializer(), error =>
				{
					exists = false;
					errorHandler(error);
				});
			}

			return exists ? items[key] : null;
		}

		/// <summary>
		/// 	Gets the value at the specified key.
		/// </summary>
		/// <typeparam name = "TKey">The type of the key.</typeparam>
		/// <typeparam name = "TValue">The type of the value.</typeparam>
		/// <param name = "items">The items.</param>
		/// <param name = "key">The key.</param>
		/// <param name = "initializer">The initializer (optional).</param>
		/// <param name = "errorHandler">The error handler (optional).</param>
		/// <returns>The requested value.</returns>
		/// <remarks>
		/// 	Error handling is provided by <see cref = "Try.HandleErrors.DefaultStrategy" />
		/// 	if no error handler is specified.
		/// </remarks>
		/// <seealso cref = "Try" />
		public static TValue GetValue<TKey, TValue>(this IDictionary<TKey, TValue> items, TKey key, Func<TValue> initializer = null, Action<Exception> errorHandler = null)
		{
			var nonGenericItems = items as IDictionary;
			Func<object> valueFactory = () => initializer == null ? default(TValue) : initializer();
			return (TValue) nonGenericItems.GetValue(key, valueFactory, errorHandler);
		}

		private static object ExtractPropertyBagValue(object value, Type propertyType)
		{
			if (propertyType == typeof (string) || propertyType.IsValueType) return value;
			var dictionary = value as IDictionary;
			if (dictionary != null) return ConvertPropertyBagDictionary(dictionary);
			return typeof (IEnumerable).IsAssignableFrom(propertyType)
			       	? (object) ConvertPropertyBagEnumerable(value as IEnumerable)
			       	: value.AsPropertyBag();
		}

		private static object[] ConvertPropertyBagEnumerable(IEnumerable set)
		{
			return set == null ? null : set.OfType<object>().Select(item => ExtractPropertyBagValue(item, item.GetType())).ToArray();
		}

		private static IDictionary<string, object> ConvertPropertyBagDictionary(IDictionary dictionary)
		{
			return dictionary == null
			       	? null
			       	: dictionary.Keys.OfType<object>()
			       	  	.ToDictionary(key => key.ToString(), key => ExtractPropertyBagValue(dictionary[key], dictionary[key].GetType()));
		}
	}
}